package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 服务实现类
 * -- 缓存穿透问题：
 * -- 当客户端请求的一个数据在缓存种和数据库种都不存在的时候 这样无论客户端发送多少个请求过来 这个缓存都不会生效
 * 都会打到数据库 其实如果数据在数据库里面存在的话 那他只有第一次才会打到数据库 接着会把它写进缓存 然后后面的就不会打到数据库了
 * -- 这样就很耗数据库性能
 * -- 解决方案：
 * -- 缓存空对象：
 * -- 你请求的时候发现缓存和数据库里面都没有 于是就缓存一个 null "" 进缓存 这样下次就直接返回空对象了 不久打到数据库
 * -- 缺点：
 * -- 有额外的内存消耗     可以给空对象添加一个有效期
 * -- 会造成数据不一致     可能缓存里面存了空对象 但是数据库里面已经插入了数据  新增数据的时候主动把数据加入缓存 即覆盖空对象
 * -- 布隆过滤器：
 * -- 类似于哈希表
 * -- 在客户端和 Redis 之间加一层 客户端先访问布隆过滤器询问这个数据是否存在 存在的话就进行接下来的操作 否则直接返回
 * -- 布隆过滤器怎么知道数据存不存在 ???
 * -- 就是添加数据的时候根据数据计算出一个哈希值 然后布隆过滤器有一个 byte 数组(哈希表) 把对应的地址的地方设置为 1
 * -- 询问数据是否存在就是根据计算出的哈希值 去看下对象的 byte 数组上那个位置是否等于 1
 * -- 由于哈希冲突的存在 布隆过滤器说存在 可能只是那个结点上为 1 但是可能是另一个数据覆盖成 1 的 所以不一定真的存在
 * -- 但是说不存在那就一定是不存在
 * -- 服务的改进：
 * -- 查询业务：
 * -- Redis 未命中 数据库查询没有这条记录的时候不再直接响应 404 而是把 null 写进 Redis
 * -- 此时 Redis 中可能命中空或者真是对象
 * -- 缓存命中的时候要看下命中的是什么 是空的话不要真的把这个空返回回去了 而是判断下 返回 404
 * -- 缓存雪崩问题：
 * -- 缓存雪崩就是同一时间内有大量的 key 失效或者 Redis 服务直接宕机 导致一下子有大量的请求到达数据库 给数据库带来瞬间的高压
 * -- 解决方案：
 * -- 避免 ttl 一起到期 可以给不同的 key 设置不同的 ttl 用随机数生成 ttl
 * -- 给 Redis 添加集群 以及 哨兵
 * -- 给缓存业务添加降级限流策略    就是知道 Redis 宕机了后就直接走快速失败 而不是去查数据库
 * -- 添加多级缓存
 * -- 缓存击穿问题：  (热点 key )
 * -- 类似于缓存雪崩问题 缓存击穿是指部分热点 key 在同一时间内过期 并且这些 key 的重建 (查数据库再往缓存中写 因为缓存数据有时候不是查到什么写什么 可能还要运算)
 * 的时间长 于是这么长的时间内 Redis 中没有这个数据 客户端这一时间内的所有请求都会访问数据库 都跑去重建缓存
 * -- 所以也可以看成是一段时间内有无数个线程跑去查数据库 然后重建缓存 解决思路是不是可以只允许一个线程去重建 其他的暂时等待
 * -- 解决方案：
 * -- 互斥锁：
 * -- 一个线程发现缓存为命中后就先获取一个锁 只有锁获取成功了 才允许去重建缓存 然后写入缓存 其他线程此时只能等待 其实获取锁失败后不应该一直重试 而是应该休眠一下
 * 于是就不会访问数据库 降低了数据库压力 但是这些线程都卡死在这里 浪费性能
 * -- 缺点：
 * -- 性能影响
 * -- 死锁风险：可能你要获取多把锁
 * -- 业务怎么改造 ???
 * -- 查询为命中的时候要先获取锁才能进行接下来的操作
 * -- Redis 实现锁：setnx 用它先设置一个值 如果成功就代表获取锁成功 如果失败代表之前有这个数据了 于是获取锁失败
 * 释放锁就是删除这个 key
 * -- 逻辑过期：
 * -- 就是不给 key 设置过期时间 而是给存入的数据里面加上一个过期时间字段 用代码逻辑去判断是否过期
 * -- 于是任何线程查缓存的时候都能命中 最多就是判断下是否过期 如果过期了就去重建缓存 重建缓存又有了锁的问题 怎么办 ???
 * 这里用一个异步的方式 他获取锁过后会去开启一个新的线程 去重建缓存 (新线程去做查数据 缓存重建 最后他来释放锁) 原来的那个数据直接返回旧数据就行
 * -- 额外的新线程在重建的时候来了其他线程 怎么办 ??? 返回旧数据 知道重建完了才能拿到新数据
 * -- 缺点：
 * -- 有个额外的字段
 * -- 数据不一致
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10) ;




    /**
     * 根据id查询商铺信息
     *
     * @param id
     * @return
     */
    public Result queryById1(Long id) throws InterruptedException {
        // 缓存穿透
         Shop shop = queryWithPassThrough(id) ;
        // 互斥锁解决缓存击穿
        //Shop shop = queryWithMutex(id);
        // 逻辑过期解决缓存击穿
        // Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        // 返回
        return Result.ok(shop);
    }

    /**
     * 封装缓存穿透逻辑
     */
    public Shop queryWithPassThrough(Long id) {
        // 键名
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从 Redis 中查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {     // 这里同时排除了 null 和 存入的 "" 空值的情况
            // 3.如果存在 直接返回
            log.info("缓存命中...");
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 这里校验下是不是查到了 "" 是的话就不查数据库
        if (shopJson != null) {
            log.info("缓存命中空数据 返回 404...");
            return null;
        }
        // 4.缓存未命中 根据 id 查询数据库
        Shop shop = getById(id);
        // 5.没查到 返回 404
        if (shop == null) {
            // 这里不存在的话直接返回  404 会出现缓存穿透问题
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);    // 写入一个 ""
            return null;
        }
        // 6.查询到的数据写入缓存
        String json = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(key, json, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);   // 添加超时时间 即超时剔除策略
        // 7.返回
        return shop;
    }

    /**
     * 互斥锁解决缓存击穿逻辑
     */
    public Shop queryWithMutex(Long id) throws InterruptedException {
        // 键名
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从 Redis 中查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {     // 这里同时排除了 null 和 存入的 "" 空值的情况
            // 3.如果存在 直接返回
            log.info("缓存命中...");
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        // 这里校验下是不是查到了 "" 是的话就不查数据库
        if (shopJson != null) {
            log.info("缓存命中空数据 返回 404...");
            return null;
        }
        // 缓存里面没找到 实现缓存重建
        // 4.1 获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;    // 每个商品有自己的锁
        Shop shop;
        try {
            Boolean isLock = tryLock(lockKey);
            // 4.2 判断锁是否获取成功
            if (!isLock) {
                // 获取失败 休眠
                Thread.sleep(50);
                // 重试
                return queryWithMutex(id);
            }
            // 4.缓存未命中 根据 id 查询数据库
            shop = getById(id);
            // 5.没查到 返回 404
            if (shop == null) {
                // 这里不存在的话直接返回  404 会出现缓存穿透问题
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);    // 写入一个 ""
                return null;
            }
            // 6.查询到的数据写入缓存
            String json = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key, json, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);   // 添加超时时间 即超时剔除策略
        } finally {
            // 释放互斥锁
            unlock(lockKey);
        }
        // 7.返回
        return shop;
    }

    /**
     * 逻辑过期解决缓存击穿逻辑
     */
    public Shop queryWithLogicalExpire( Long id ) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在，直接返回
            return null;
        }
        // 4.命中，需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期，直接返回店铺信息
            return shop;
        }
        // 5.2.已过期，需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            CACHE_REBUILD_EXECUTOR.submit( ()->{

                try{
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return shop;
    }

    /**
     * 根据id查询商铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) throws InterruptedException {
        String key = RedisConstants.CACHE_SHOP_KEY + id ;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        Shop shop = getById(id) ;
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    /**
     * 更新商铺信息
     *
     * @param shop
     * @return
     */
    @Override
    @Transactional      // 更新数据库和缓存要一起成功
    public Result update(Shop shop) {
        // 1.判断商铺书否存在
        Long shopId = shop.getId();
        if (shopId == null) {
            return Result.fail("商铺不存在");
        }
        // 2.更新数据库
        updateById(shop);
        // 3.删除 Redis 中的缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shopId);
        // 4.返回
        return Result.ok();
    }

    /**
     * 执行缓存重建
     */
    public void saveShop2Redis(Long id, Long expireSeconds) {
        // 查询店铺数据
        Shop shop = getById(id);
        // 封装消息
        RedisData redisData = RedisData.builder()
                .data(shop)
                .expireTime(LocalDateTime.now().plusSeconds(expireSeconds))    // 当前时间加上
                .build();
        // 写入 Redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 获取锁
     */
    private Boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        // 包装类用工具类返回 避免空指针
        return BooleanUtil.isTrue(flag);
    }


    /**
     * 释放锁
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
